package com.ieducc.esp_blufi;

import static android.content.Context.LOCATION_SERVICE;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.Context;
import android.location.LocationManager;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.location.LocationManagerCompat;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import blufi.espressif.BlufiCallback;
import blufi.espressif.BlufiClient;
import blufi.espressif.params.BlufiConfigureParams;
import blufi.espressif.params.BlufiParameter;
import blufi.espressif.response.BlufiScanResult;
import blufi.espressif.response.BlufiStatusResponse;
import blufi.espressif.response.BlufiVersionResponse;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;

/** EspBlufiPlugin */
public class EspBlufiPlugin implements FlutterPlugin, MethodCallHandler {
  /// The MethodChannel that will the communication between Flutter and native Android
  ///
  /// This local reference serves to register the plugin with the Flutter Engine and unregister it
  /// when the Flutter Engine is detached from the Activity
  final String TAG = "ESP_BLUFI";

  private MethodChannel channel;
  private Context context;

  private Handler mHandler;
//  private List<ScanResult> mBleList;

  private Map<String, ScanResult> mDeviceMap;
  private ScanCallback mScanCallback;
  private String mBlufiFilter;
  private BluetoothDevice mDevice;
  private BlufiClient mBlufiClient;

  @Override
  public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
    mHandler = new Handler(Looper.getMainLooper());
    context = flutterPluginBinding.getApplicationContext();
    channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "esp_blufi");
    channel.setMethodCallHandler(this);
  }

  @Override
  public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
    Log.d(TAG, call.method);
    if (call.method.equals("getPlatformVersion")) {
      result.success("Android " + android.os.Build.VERSION.RELEASE);
    } else if (call.method.equals("isBluetoothEnabled")){
      isBluetoothEnabled(result);
    } else if (call.method.equals("openBluetooth")){
      openBluetooth(result);
    } else if (call.method.equals("scan")){
      startScan(call.argument("filter"), result);
    } else if (call.method.equals("stopScan")){
      stopScan();
    } else if (call.method.equals("connectDevice")){
      connectDevice(mDeviceMap.get(call.argument("address")).getDevice());
    } else if (call.method.equals("configure")){
      configure(call.argument("ssid"), call.argument("password"));
    } else if (call.method.equals("requestDeviceWifiScan")){
      requestDeviceWifiScan();
    } else if (call.method.equals("requestDeviceVersion")){
      requestDeviceVersion();
    } else if (call.method.equals("requestDeviceStatus")){
      requestDeviceStatus();
    } else if (call.method.equals("requestCloseConnection")){
      requestCloseConnection();
    }
    else {
      result.notImplemented();
    }
  }

  private void isBluetoothEnabled(Result result) {
    BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
    result.success(adapter != null && adapter.isEnabled());
  }

  private void openBluetooth(Result result) {
    BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
    result.success(adapter.enable());
  }

  private void startScan(String prefix, Result result){
    mScanCallback = new ScanCallback();
    mDeviceMap = new HashMap<>();
    BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
    BluetoothLeScanner scanner = adapter.getBluetoothLeScanner();
    if (!adapter.isEnabled() || scanner == null) {
//      Toast.makeText(context, "蓝牙不可用", Toast.LENGTH_SHORT).show();
      result.error(""+EspBlufiCode.BLE_DISABLED, null, null);
      Log.w(TAG, "ble disabled");
      return;
    }
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
      // Check location enable
      LocationManager locationManager = (LocationManager) context.getSystemService(LOCATION_SERVICE);
      boolean locationEnable = locationManager != null && LocationManagerCompat.isLocationEnabled(locationManager);
      if (!locationEnable) {
        result.error(""+EspBlufiCode.LOCATION_DISABLED, null, null);
//        Toast.makeText(context, "位置信息(GPS)不可用", Toast.LENGTH_SHORT).show();
        Log.w(TAG, "location disabled");
        return;
      }
    }
    mDeviceMap.clear();
//    mBleList.clear();
    mBlufiFilter = prefix;
    scanner.startScan(null, new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build(),
            mScanCallback);
    result.success(true);
  }

  private void stopScan(){
    BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
    BluetoothLeScanner scanner = adapter.getBluetoothLeScanner();
    if (scanner != null) {
      scanner.stopScan(mScanCallback);
    }
  }

  // Try to connect device
  private void connectDevice(BluetoothDevice device){
    mDevice = device;
    if (mBlufiClient != null){
      mBlufiClient.close();
      mBlufiClient = null;
    }
    mBlufiClient = new BlufiClient(context, mDevice);
    mBlufiClient.setGattCallback(new GattCallback());
    mBlufiClient.setBlufiCallback(new BlufiCallbackMain());
    mBlufiClient.setGattWriteTimeout(EspBlufiConstants.GATT_WRITE_TIMEOUT);
    mBlufiClient.connect();
  }

  private void configure(String ssid, String password){
    BlufiConfigureParams params = new BlufiConfigureParams();
    params.setOpMode(BlufiParameter.OP_MODE_STA);
//    params.setStaBSSID(ssid);
    params.setStaSSIDBytes(ssid.getBytes());
    params.setStaPassword(password);
    mBlufiClient.configure(params);
  }

  private void requestDeviceWifiScan() {
    mBlufiClient.requestDeviceWifiScan();
  }

  private void requestDeviceVersion(){
    mBlufiClient.requestDeviceVersion();
  }

  private void requestDeviceStatus(){
    mBlufiClient.requestDeviceStatus();
  }

  private void requestCloseConnection() {
    mBlufiClient.requestCloseConnection();
  }

  void responseMessage(@NonNull String method, @Nullable Object arguments){
    mHandler.post(() -> channel.invokeMethod(method, arguments));
  }

  @Override
  public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
    channel.setMethodCallHandler(null);
  }


  private class ScanCallback extends android.bluetooth.le.ScanCallback {

    @Override
    public void onScanFailed(int errorCode) {
      super.onScanFailed(errorCode);
    }

    @Override
    public void onBatchScanResults(List<ScanResult> results) {
      for (ScanResult result : results) {
        onLeScan(result);
      }
    }

    @Override
    public void onScanResult(int callbackType, ScanResult result) {
      onLeScan(result);
    }

    private void onLeScan(ScanResult scanResult) {
      BluetoothDevice device = scanResult.getDevice();

      String name = scanResult.getDevice().getName();
      if (!TextUtils.isEmpty(mBlufiFilter)) {
        if (name == null || !name.startsWith(mBlufiFilter)) {
          return;
        }
      }
      Map deviceMap = new HashMap();
      deviceMap.put("name", device.getName());
      deviceMap.put("address", device.getAddress());
      deviceMap.put("rssi", scanResult.getRssi());
      responseMessage("scanResult", deviceMap);
      mDeviceMap.put(scanResult.getDevice().getAddress(), scanResult);
    }
  }


  /**
   * mBlufiClient call onCharacteristicWrite and onCharacteristicChanged is required
   */
  private class GattCallback extends BluetoothGattCallback {
    @Override
    public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
      String devAddr = gatt.getDevice().getAddress();
      Log.d(TAG, String.format(Locale.ENGLISH, "onConnectionStateChange addr=%s, status=%d, newState=%d",
              devAddr, status, newState));
      HashMap map = new HashMap();
      if (status == BluetoothGatt.GATT_SUCCESS) {
        switch (newState) {
          case BluetoothProfile.STATE_CONNECTED:
            Log.i(TAG, String.format("Connected %s", devAddr));

            map.put("code", EspBlufiCode.SUCCESS);
            responseMessage("connectDevice", map);
//            onGattConnected();
//            updateMessage(String.format("Connected %s", devAddr), false);
            break;
          case BluetoothProfile.STATE_DISCONNECTED:
            gatt.close();
            Log.w(TAG, String.format("Disconnected %s", devAddr));
            map.put("code", EspBlufiCode.DISCONNECTED);
            responseMessage("connectDevice", map);
//            onGattDisconnected();
//            updateMessage(String.format("Disconnected %s", devAddr), false);
            break;
        }
      } else {
        gatt.close();
        Log.w(TAG, String.format(Locale.ENGLISH, "Disconnect %s, status=%d", devAddr, status));
        map.put("code", EspBlufiCode.CONNECT_FAIL);
        responseMessage("connectDevice", map);
//        onGattDisconnected();
//        updateMessage(String.format(Locale.ENGLISH, "Disconnect %s, status=%d", devAddr, status),false);
      }
    }

    @Override
    public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
      Log.d(TAG, String.format(Locale.ENGLISH, "onMtuChanged status=%d, mtu=%d", status, mtu));
      if (status == BluetoothGatt.GATT_SUCCESS) {
//        updateMessage(String.format(Locale.ENGLISH, "Set mtu complete, mtu=%d ", mtu), false);
      } else {
        mBlufiClient.setPostPackageLengthLimit(20);
//        updateMessage(String.format(Locale.ENGLISH, "Set mtu failed, mtu=%d, status=%d", mtu, status), false);
      }

//      onGattServiceCharacteristicDiscovered();
    }

    @Override
    public void onServicesDiscovered(BluetoothGatt gatt, int status) {
      Log.d(TAG, String.format(Locale.ENGLISH, "onServicesDiscovered status=%d", status));
      if (status != BluetoothGatt.GATT_SUCCESS) {
        gatt.disconnect();
//        updateMessage(String.format(Locale.ENGLISH, "Discover services error status %d", status), false);
      }
    }

    @Override
    public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
      Log.d(TAG, String.format(Locale.ENGLISH, "onDescriptorWrite status=%d", status));
      if (descriptor.getUuid().equals(BlufiParameter.UUID_NOTIFICATION_DESCRIPTOR) &&
              descriptor.getCharacteristic().getUuid().equals(BlufiParameter.UUID_NOTIFICATION_CHARACTERISTIC)) {
        String msg = String.format(Locale.ENGLISH, "Set notification enable %s", (status == BluetoothGatt.GATT_SUCCESS ? " complete" : " failed"));
        Log.d(TAG, msg);
//        updateMessage(msg, false);
      }
    }

    @Override
    public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
      if (status != BluetoothGatt.GATT_SUCCESS) {
        gatt.disconnect();
        Log.d(TAG, "onCharacteristicWrite");
//        updateMessage(String.format(Locale.ENGLISH, "WriteChar error status %d", status), false);
      }
    }
  }

  private class BlufiCallbackMain extends BlufiCallback {
    @Override
    public void onGattPrepared(
            BlufiClient client,
            BluetoothGatt gatt,
            BluetoothGattService service,
            BluetoothGattCharacteristic writeChar,
            BluetoothGattCharacteristic notifyChar
    ) {
      if (service == null) {
        Log.w(TAG, "Discover service failed");
        gatt.disconnect();
//        updateMessage("Discover service failed", false);
        return;
      }
      if (writeChar == null) {
        Log.w(TAG, "Get write characteristic failed");
        gatt.disconnect();
//        updateMessage("Get write characteristic failed", false);
        return;
      }
      if (notifyChar == null) {
        Log.w(TAG, "Get notification characteristic failed");
        gatt.disconnect();
//        updateMessage("Get notification characteristic failed", false);
        return;
      }
      Log.d(TAG, "Discover service and characteristics success");
//      updateMessage("Discover service and characteristics success", false);

      int mtu = EspBlufiConstants.DEFAULT_MTU_LENGTH;
      Log.d(TAG, "Request MTU " + mtu);
      boolean requestMtu = gatt.requestMtu(mtu);
      if (!requestMtu) {
        Log.w(TAG, "Request mtu failed");
//        updateMessage(String.format(Locale.ENGLISH, "Request mtu %d failed", mtu), false);
//        onGattServiceCharacteristicDiscovered();
      }
    }

    @Override
    public void onNegotiateSecurityResult(BlufiClient client, int status) {
      if (status == STATUS_SUCCESS) {
        Log.d(TAG, "Negotiate security complete");
//        updateMessage("Negotiate security complete", false);
      } else {
        Log.d(TAG, "Negotiate security failed， code=" + status);
//        updateMessage("Negotiate security failed， code=" + status, false);
      }

//      mContent.blufiSecurity.setEnabled(mConnected);
    }

    @Override
    public void onPostConfigureParams(BlufiClient client, int status) {
      if (status == STATUS_SUCCESS) {
        Log.d(TAG, "Post configure params complete");
//        updateMessage("Post configure params complete", false);
      } else {
        Log.w(TAG, "Post configure params failed, code=" + status);
//        updateMessage("Post configure params failed, code=" + status, false);
      }

//      mContent.blufiConfigure.setEnabled(mConnected);
    }

    @Override
    public void onDeviceStatusResponse(BlufiClient client, int status, BlufiStatusResponse response) {
      HashMap map = new HashMap();
      if (status == STATUS_SUCCESS) {
        map.put("code", EspBlufiCode.SUCCESS);
        map.put("opMode", response.getOpMode());
        map.put("bssid", response.getStaBSSID());
        map.put("ssid", response.getStaSSID());
        map.put("password", response.getStaPassword());
        map.put("message", response.generateValidInfo());
        responseMessage("deviceStatus", map);
        Log.d(TAG, String.format("Receive device status response:\n%s", response.generateValidInfo()));
//        updateMessage(String.format("Receive device status response:\n%s", response.generateValidInfo()),
//                true);
      } else {
        map.put("code", EspBlufiCode.DEVICE_STATUS_FAIL);
        responseMessage("deviceStatus", map);
        Log.d(TAG, "Device status response error, code=" + status);
//        updateMessage("Device status response error, code=" + status, false);
      }

//      mContent.blufiDeviceStatus.setEnabled(mConnected);
    }

    @Override
    public void onDeviceScanResult(BlufiClient client, int status, List<BlufiScanResult> results) {
      if (status == STATUS_SUCCESS) {
        StringBuilder msg = new StringBuilder();
        msg.append("Receive device scan result:\n");
        for (BlufiScanResult scanResult : results) {
          msg.append(scanResult.toString()).append("\n");
        }

        ArrayList<HashMap> resultList = new ArrayList();
        for (int i = 0; i < results.size(); i++) {
          BlufiScanResult result = results.get(i);
          HashMap map = new HashMap();
          map.put("ssid", result.getSsid());
          map.put("rssi", result.getRssi());
          map.put("type", result.getType());
          resultList.add(map);
        }
        responseMessage("scanWifi", resultList);
//        updateMessage(msg.toString(), true);
      } else {
        Log.w(TAG, "Device scan result error, code=" + status);
//        updateMessage("Device scan result error, code=" + status, false);
      }

//      mContent.blufiDeviceScan.setEnabled(mConnected);
    }

    @Override
    public void onDeviceVersionResponse(BlufiClient client, int status, BlufiVersionResponse response) {
      if (status == STATUS_SUCCESS) {
        HashMap map = new HashMap();
        map.put("code", EspBlufiCode.SUCCESS);
        map.put("message", response.getVersionString());
        responseMessage("deviceVersion", map);
        Log.d(TAG, String.format("Receive device version: %s", response.getVersionString()));
//        updateMessage(String.format("Receive device version: %s", response.getVersionString()),
//                true);
      } else {
        HashMap map = new HashMap();
        map.put("code", EspBlufiCode.DEVICE_VERSION_FAIL);
        responseMessage("deviceVersion", map);
        Log.w(TAG, "Device version error, code=" + status);
//        updateMessage("Device version error, code=" + status, false);
      }

//      mContent.blufiVersion.setEnabled(mConnected);
    }

    @Override
    public void onPostCustomDataResult(BlufiClient client, int status, byte[] data) {
      String dataStr = new String(data);
      String format = "Post data %s %s";
      if (status == STATUS_SUCCESS) {
        Log.d(TAG, String.format(format, dataStr, "complete"));
//        updateMessage(String.format(format, dataStr, "complete"), false);
      } else {
        Log.w(TAG, String.format(format, dataStr, "failed"));
//        updateMessage(String.format(format, dataStr, "failed"), false);
      }
    }

    @Override
    public void onReceiveCustomData(BlufiClient client, int status, byte[] data) {
      if (status == STATUS_SUCCESS) {
        String customStr = new String(data);
        Log.d(TAG, String.format("Receive custom data:\n%s", customStr));
//        updateMessage(String.format("Receive custom data:\n%s", customStr), true);
      } else {
        Log.w(TAG, "Receive custom data error, code=" + status);
//        updateMessage("Receive custom data error, code=" + status, false);
      }
    }

    @Override
    public void onError(BlufiClient client, int errCode) {
      Log.w(TAG, String.format(Locale.ENGLISH, "Receive error code %d", errCode));
//      updateMessage(String.format(Locale.ENGLISH, "Receive error code %d", errCode), false);
      if (errCode == CODE_GATT_WRITE_TIMEOUT) {
        Log.w(TAG, "Gatt write timeout");
//        updateMessage("Gatt write timeout", false);
        client.close();
//        onGattDisconnected();
      } else if (errCode == 11) {
        Log.w(TAG, "Scan failed, please retry later");
//        updateMessage("Scan failed, please retry later", false);
//        mContent.blufiDeviceScan.setEnabled(true);
      }
    }
  }
}
